/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.basics.manager.impl;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import com.genia.toolbox.basics.bean.InputStreamFactory;
import com.genia.toolbox.basics.bean.StreamProgressListener;
import com.genia.toolbox.basics.bean.impl.CacheInputStreamFactory;
import com.genia.toolbox.basics.bean.impl.StreamProgressAdapter;
import com.genia.toolbox.basics.exception.technical.TechnicalIOException;
import com.genia.toolbox.basics.manager.ByteArrayProvider;
import com.genia.toolbox.basics.manager.ExceptionManager;
import com.genia.toolbox.basics.manager.FileManager;
import com.genia.toolbox.basics.manager.StreamManager;
import com.genia.toolbox.constants.client.Charset;

/**
 * Default implementation of <code>StreamManager</code>.
 * 
 * @see com.genia.toolbox.basics.manager.StreamManager
 */
public class StreamManagerImpl
    implements StreamManager
{

  /**
   * the minimum time in ms between two updates to a listener.
   */
  private static final int TIME_BETWEEN_PROGRESS_UPDATE = 100;

  /**
   * the <code>ByteArrayProvider</code> to retrieve byte arrays.
   */
  private ByteArrayProvider byteArrayProvider;

  /**
   * the {@link ExceptionManager} to use.
   */
  private ExceptionManager exceptionManager;

  /**
   * the {@link FileManager} to use.
   */
  private FileManager fileManager;

  /**
   * a {@link StreamProgressListener} that does nothing.
   */
  private static final StreamProgressListener NULL_PROGESS_LISTENER = new StreamProgressAdapter();



  /**
   * * Copy the content of an <code>InputStream</code> to an
   * <code>OutputStream</code>.
   * 
   * @param input
   *          the <code>InputStream</code>
   * @param output
   *          the <code>OutputStream</code>
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#copyStream(InputStream,
   *      OutputStream)
   */
  public void copyStream(final InputStream input, final OutputStream output)
      throws TechnicalIOException
  {
    copyStream(input, output, NULL_PROGESS_LISTENER);
  }



  /**
   * Copy at most <code>byteCount</code> (<code>-1</code> means no limit)
   * of content from an <code>InputStream</code> to an
   * <code>OutputStream</code>.
   * 
   * @param input
   *          the <code>InputStream</code>
   * @param output
   *          the <code>OutputStream</code>
   * @param byteCount
   *          number of byte to copie (<code>-1</code> mean no limit)
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#copyStream(InputStream,
   *      OutputStream, long)
   */
  public void copyStream(final InputStream input, final OutputStream output, final long byteCount)
      throws TechnicalIOException
  {
    copyStream(input, output, byteCount, NULL_PROGESS_LISTENER);
  }



  /**
   * getter for the byteArrayProvider property.
   * 
   * @return the byteArrayProvider
   */
  public ByteArrayProvider getByteArrayProvider()
  {
    return byteArrayProvider;
  }



  /**
   * setter for the byteArrayProvider property.
   * 
   * @param byteArrayProvider
   *          the byteArrayProvider to set
   */
  public void setByteArrayProvider(final ByteArrayProvider byteArrayProvider)
  {
    this.byteArrayProvider = byteArrayProvider;
  }



  /**
   * Return the content of the input as in byte array.
   * 
   * @param input
   *          the <code>InputStream</code>
   * @return a byte array containing the content of the input
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#getBytes(java.io.InputStream)
   */
  public byte[] getBytes(final InputStream input)
      throws TechnicalIOException
  {
    return getBytes(input, NULL_PROGESS_LISTENER);
  }



  /**
   * returns an {@link InputStreamFactory} that generates clone of the given
   * {@link InputStream}. This method use the {@link InputStream} given in
   * parameter.
   * 
   * @param inputStream
   *          the {@link InputStream} to clone
   * @return an {@link InputStreamFactory} that generate clones of the
   *         {@link InputStream} in parameter
   * @throws TechnicalIOException
   *           when an error occured
   * @see com.genia.toolbox.basics.manager.StreamManager#cloneInputStream(java.io.InputStream)
   */
  public InputStreamFactory cloneInputStream(InputStream inputStream)
      throws TechnicalIOException
  {
    try {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      copyStream(inputStream, baos, 1024 * 1024);
      final File file = getFileManager().createAutoDeletableTempFile("cache", ".bin");
      copyStream(inputStream, new FileOutputStream(file));
      return new CacheInputStreamFactory(baos.toByteArray(), file);
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the exceptionManager property.
   * 
   * @return the exceptionManager
   */
  public ExceptionManager getExceptionManager()
  {
    return exceptionManager;
  }



  /**
   * setter for the exceptionManager property.
   * 
   * @param exceptionManager
   *          the exceptionManager to set
   */
  public void setExceptionManager(ExceptionManager exceptionManager)
  {
    this.exceptionManager = exceptionManager;
  }



  /**
   * returns the {@link InputStream} that is the content of the file at the
   * given path in the zip that is represented by the {@link InputStream} given
   * in parameter.
   * 
   * @param zipInputStream
   *          the {@link InputStream} containing the Zip to search
   * @param filePath
   *          the path of the file to extract
   * @return the {@link InputStream} that is the content of the file at the
   *         given path in the zip that is represented by the
   *         {@link InputStream} given in parameter, or <code>null</code> if
   *         either the file is not a Zip file, or the given file does not exist
   *         in the Zip
   * @throws TechnicalIOException
   *           if an error occured
   * @see com.genia.toolbox.basics.manager.StreamManager#getInputStreamFromZip(java.io.InputStream,
   *      java.lang.String)
   */
  public InputStream getInputStreamFromZip(InputStream zipInputStream, String filePath)
      throws TechnicalIOException
  {
    try {
      ZipInputStream result = new ZipInputStream(zipInputStream);
      ZipEntry zipEntry = result.getNextEntry();
      while (zipEntry != null) {
        if (zipEntry.getName().equals(filePath)) {
          return result;
        }
        zipEntry = result.getNextEntry();
      }
      return null;
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }
  }



  /**
   * getter for the fileManager property.
   * 
   * @return the fileManager
   */
  public FileManager getFileManager()
  {
    return fileManager;
  }

  /**
   * the different state of the charset parser.
   */
  private enum CharsetParserState
  {
    /**
     * Starting point.
     */
    STARTING,
    /**
     * After the first byte of a 2 bytes utf8 sequence.
     */
    UTF8_2_1,
    /**
     * After the first byte of a 3 bytes utf8 sequence.
     */
    UTF8_3_1,
    /**
     * After the second byte of a 3 bytes utf8 sequence.
     */
    UTF8_3_2,
    /**
     * After the first byte of a 4 bytes utf8 sequence.
     */
    UTF8_4_1,
    /**
     * After the second byte of a 4 bytes utf8 sequence.
     */
    UTF8_4_2,
    /**
     * After the third byte of a 4 bytes utf8 sequence.
     */
    UTF8_4_3;
  }



  /**
   * returns the charset of the given {@link InputStream}. The rules is the
   * following: if no byte has a value greater than 127, the charset is ASCII,
   * otherwise if the stream is a correct UTF8 stream, UTF8 is returned.
   * Finally, ISO-8859-1 is returned.
   * 
   * @param inputStream
   *          the {@link InputStream} to check
   * @return the charset of the given {@link InputStream}
   * @throws TechnicalIOException
   *           when an error occured
   * @see StreamManager#getCharset(InputStream)
   */
  public String getCharset(InputStream inputStream)
      throws TechnicalIOException
  {
    byte[] buffer = getByteArrayProvider().getArray();
    try {
      CharsetParserState state = CharsetParserState.STARTING;
      String charset = Charset.US_ASCII;
      int size = 0;
      do {

        size = inputStream.read(buffer);
        for (int index = 0; index < size; index++) {
          byte currentByte = buffer[index];
          switch (state) {
            case STARTING:
              // Bytes are signed.
              if (currentByte < 0) {
                charset = Charset.UTF8;
                if ((0xe0 & currentByte) == 0xc0) {
                  state = CharsetParserState.UTF8_2_1;
                  break;
                }
                if ((0xf0 & currentByte) == 0xe0) {
                  state = CharsetParserState.UTF8_3_1;
                  break;
                }
                if ((0xf8 & currentByte) == 0xf0) {
                  state = CharsetParserState.UTF8_4_1;
                  break;
                }
                return Charset.ISO_8859_1;
              }
              break;
            case UTF8_2_1:
            case UTF8_3_2:
            case UTF8_4_3:
              if ((0xc0 & currentByte) == 0x80) {
                state = CharsetParserState.STARTING;
                break;
              }
              return Charset.ISO_8859_1;
            default:
              if ((0xc0 & currentByte) == 0x80) {
                state = CharsetParserState.values()[state.ordinal() + 1];
                break;
              }
              return Charset.ISO_8859_1;
          }
        }
      }
      while (size >= 0);
      if (state == CharsetParserState.STARTING) {
        return charset;
      }
      return Charset.ISO_8859_1;
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }
    finally {
      getByteArrayProvider().release(buffer);
    }
  }



  /**
   * setter for the fileManager property.
   * 
   * @param fileManager
   *          the fileManager to set
   */
  public void setFileManager(FileManager fileManager)
  {
    this.fileManager = fileManager;
  }



  /**
   * Copy the content of an {@link InputStream} to an {@link OutputStream}.
   * 
   * @param input
   *          the {@link InputStream}
   * @param output
   *          the {@link OutputStream}
   * @param progressListener
   *          the {@link StreamProgressListener} to report to, it must not be
   *          <code>null</code>
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#copyStream(java.io.InputStream,
   *      java.io.OutputStream,
   *      com.genia.toolbox.basics.bean.StreamProgressListener)
   */
  public void copyStream(InputStream input, OutputStream output, StreamProgressListener progressListener)
      throws TechnicalIOException
  {
    copyStream(input, output, -1, progressListener);
  }



  /**
   * Copy at most <code>byteCount</code> (<code>-1</code> means no limit)
   * of content from an {@link InputStream} to an {@link OutputStream}.
   * 
   * @param input
   *          the {@link InputStream}
   * @param output
   *          the {@link OutputStream}
   * @param byteCount
   *          number of byte to copie (<code>-1</code> mean no limit)
   * @param progressListener
   *          the {@link StreamProgressListener} to report to, it must not be
   *          <code>null</code>
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#copyStream(java.io.InputStream,
   *      java.io.OutputStream, long,
   *      com.genia.toolbox.basics.bean.StreamProgressListener)
   */
  public void copyStream(InputStream input, OutputStream output, long byteCount, StreamProgressListener progressListener)
      throws TechnicalIOException
  {
    byte buffer[] = null;
    long currentByteCount = byteCount;
    long bytesRead = 0;
    long lastUpdated = System.currentTimeMillis();

    progressListener.update(bytesRead);

    try {
      buffer = getByteArrayProvider().getArray();
      int len = buffer.length;

      if (currentByteCount >= 0) {
        while (currentByteCount > 0) {
          if (currentByteCount < len) {
            len = input.read(buffer, 0, (int) currentByteCount);
          }
          else {
            len = input.read(buffer, 0, buffer.length);
          }

          if (len == -1) {
            break;
          }

          currentByteCount -= len;
          output.write(buffer, 0, len);
          bytesRead += len;
          lastUpdated = updateIfNecessary(progressListener, bytesRead, lastUpdated);
        }
      }
      else {
        while (true) {
          len = input.read(buffer, 0, buffer.length);
          if (len < 0) {
            break;
          }
          output.write(buffer, 0, len);
          bytesRead += len;
          lastUpdated = updateIfNecessary(progressListener, bytesRead, lastUpdated);
        }
      }
    }
    catch (final IOException e) {
      throw getExceptionManager().convertException(e);
    }
    finally {
      getByteArrayProvider().release(buffer);
    }

  }



  /**
   * update the {@link StreamProgressListener} if necessary.
   * 
   * @param progressListener
   *          the {@link StreamProgressListener} to update
   * @param bytesRead
   *          the number of read bytes
   * @param lastUpdated
   *          the time of the last update
   * @return the time of the last update
   */
  private long updateIfNecessary(StreamProgressListener progressListener, long bytesRead, long lastUpdated)
  {
    long newTime = System.currentTimeMillis();
    if (newTime - lastUpdated > TIME_BETWEEN_PROGRESS_UPDATE) {
      progressListener.update(bytesRead);
      return newTime;
    }
    return lastUpdated;
  }



  /**
   * Return the content of the input as in byte array.
   * 
   * @param input
   *          the {@link InputStream}
   * @return a byte array containing the content of the input
   * @param progressListener
   *          the {@link StreamProgressListener} to report to, it must not be
   *          <code>null</code>
   * @throws TechnicalIOException
   *           if an Input/Output error occurs
   * @see com.genia.toolbox.basics.manager.StreamManager#getBytes(java.io.InputStream,
   *      com.genia.toolbox.basics.bean.StreamProgressListener)
   */
  public byte[] getBytes(InputStream input, StreamProgressListener progressListener)
      throws TechnicalIOException
  {
    final ByteArrayOutputStream baos = new ByteArrayOutputStream();
    copyStream(input, baos, progressListener);
    return baos.toByteArray();
  }

}
